<?php

class DataListField extends FormField {
    private static $allowed_actions = array('export');
	
	protected $allowedExport = true;
    /**
     * The {@link DataList} object defining the source data for this view/
     */
    protected $dataList;
    
    protected $fieldList;
    
    /**
     * @var $fieldListCsv array
     */
    protected $fieldListCsv;
    
    /**
     * @var $summaryFieldList array Shows a row which summarizes the contents of a column by a predefined
     * Javascript-function 
     */
    protected $summaryFieldList;
    
    /**
     * @var $summaryTitle string The title which will be shown in the first column of the summary-row.
     * Accordingly, the first column can't be used for summarizing.
     */
    protected $summaryTitle;
    
    /**
     * @var $template string Template-Overrides
     */
    protected $template = "DataListField";
    
    /**
     * @var $itemClass string Class name for each item/row
     */
    public $itemClass = 'DataListField_Item';

    /**
     * @var $customCsvQuery Query for CSV-export (might need different fields or further filtering)
     */
    protected $customCsvQuery;
    
    /**
     * Character to seperate exported columns in the CSV file
     */
    protected $csvSeparator = ",";
    
    /*
     * Boolean deciding whether to include a header row in the CSV file
     */
    protected $csvHasHeader = true;
    
    /**
     * @var array Specify custom escape for the fields.
     *
     * <code>
     * array("\""=>"\"\"","\r"=>"", "\r\n"=>"", "\n"=>"")
     * </code>
     */
    public $csvFieldEscape = array(
        "\""=>"\"\"",
        "\r\n"=>"", 
        "\r"=>"",
        "\n"=>"",
    );
    
    /**
     * @var boolean Trigger pagination
     */
    protected $showPagination = true;
    
    /**
     * @var string Override the {@link Link()} method
     * for all pagination. Useful to force rendering of the field
     * in a different context.
     */
    public $paginationBaseLink = null;
    
    /**
     * @var int Number of items to show on a single page (needed for pagination)
     */
    protected $pageSize = 10;
    
    /**
     * @var array Specify castings with fieldname as the key, and the desired casting as value.
     * Example: array("MyCustomDate"=>"Date","MyShortText"=>"Text->FirstSentence")
     */
    public $fieldCasting = array();
    
    /**
     * @var array Specify custom formatting for fields, e.g. to render a link instead of pure text.
     * Caution: Make sure to escape special php-characters like in a normal php-statement. 
     * Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'
     */
    public $fieldFormatting = array();
    
    public $csvFieldFormatting = array();
    
    /**
     * @var string $groupByField Used to group by a specific column in the DataObject
     * and create partial summaries.
     */
    public $groupByField = null;
    
    /**
     * @var array
     */
    protected $extraLinkParams;
    
    protected $__cachedQuery;
    
    /**
     * This is a flag that enables some backward-compatibility helpers.
     */
    private $getDataListFromForm;
    
    /**
     * @param $name string The fieldname
     * @param $sourceClass string The source class of this field
     * @param $fieldList array An array of field headings of Fieldname => Heading Text (eg. heading1)
     * @param $sourceFilter string The filter field you wish to limit the objects by (eg. parentID)
     * @param $sourceSort string
     * @param $sourceJoin string
     */
    public function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null,
        $sourceSort = null, $sourceJoin = null) {

        if($sourceClass) {
            // You can optionally pass a list
            if($sourceClass instanceof SS_List) {
                $this->dataList = $sourceClass;
                
            } else {
                $this->dataList = DataList::create($sourceClass)->where($sourceFilter)->sort($sourceSort);
                if($sourceJoin) $this->dataList = $this->dataList->join($sourceJoin);
                // Grab it from the form relation, if available.
                $this->getDataListFromForm = true;
            }
        }

        $this->fieldList = ($fieldList) ? $fieldList : singleton($this->datalist->dataClass())->summaryFields();

        parent::__construct($name);
    }
    
    public function index() {
        return $this->FieldHolder();
    }

    private static $url_handlers = array(
        'item/$ID' => 'handleItem',
        '$Action' => '$Action',
    );

    public function sourceClass() {
        $list = $this->getDataList();
        if(method_exists($list, 'dataClass')) return $list->dataClass();
        // Failover for SS_List
        else return get_class($list->First());
    }
    
    public function handleItem($request) {
        return new DataListField_ItemRequest($this, $request->param('ID'));
    }

    function FieldHolder($properties = array()) {
        Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
        Requirements::javascript('datalist/javascript/DataListField.js');
        Requirements::css('datalist/css/DataListField.css');
        return $this->renderWith($this->template);
    }
    
    public function Headings() {
        $headings = array();
        foreach($this->fieldList as $fieldName => $fieldTitle) {
            $fieldClasses = "";
            if(is_array($fieldTitle)){
                $fieldClasses = is_array($fieldTitle['classes']) ? implode(" ", $fieldTitle['classes']) : $fieldTitle['classes'];
                $fieldTitle = $fieldTitle['title'];
            }
            
            $headings[] = new ArrayData(array(
                "Name" => $fieldName, 
                "Classes" => $fieldClasses,
                "Title" => ($this->sourceClass()) 
                    ? singleton($this->sourceClass())->fieldLabel($fieldTitle)
                    : $fieldTitle
            ));
        }
        return new ArrayList($headings);
    }
    
    /**
     * Provide a custom query to compute sourceItems. This is the preferred way to using
     * {@setSourceItems}, because we can still paginate.
     * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY).  
     * 
     * @param $query DataList
     */
    public function setCustomQuery(DataList $dataList) {
        $this->dataList = $dataList;
        return $this;
    }

    public function setCustomCsvQuery(DataList $dataList) {
        $this->customCsvQuery = $query;
        return $this;
    }
    
    /**
     * Get items, with sort & limit applied
     */
    public function sourceItems() {
        // get items (this may actually be a SS_List)
        $items = clone $this->getDataList();

        // Determine pagination limit, offset
        // To disable pagination, set $this->showPagination to false.
        if($this->showPagination && $this->pageSize) {
            $SQL_limit = (int)$this->pageSize;
            if(isset($_REQUEST['ctf'][$this->getName()]['start']) && is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) {

                if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
                    $SQL_start = intval($_REQUEST['ctf'][$this->getName()]['start']);
                }
                else {
                    $SQL_start = "0";
                }
            }
            else {
                $SQL_start = 0;
            }

            $items = $items->limit($SQL_limit, $SQL_start);
        }

        return $items;
    }

    /**
     * Return a SS_List of DataListField_Item objects, suitable for display in the template.
     */
    public function Items() {
        $fieldItems = new ArrayList();
        if($items = $this->sourceItems()) foreach($items as $item) {
            if($item) $fieldItems->push(new $this->itemClass($item, $this));
        }
        return $fieldItems;
    }
    
    /**
     * Returns the DataList for this field.
     */
    public function getDataList() {
        // If we weren't passed in a DataList to begin with, try and get the datalist from the form
        if($this->form && $this->getDataListFromForm) {
            $this->getDataListFromForm = false;
            $this->dataList = $this->form->getDataList();
        }
        
        if(!$this->dataList) {
            user_error(get_class($this). ' is missing a DataList', E_USER_ERROR);
        }
        
        return $this->dataList;
    }

    public function getCsvDataList() {
        if($this->customCsvQuery) return $this->customCsvQuery;
        else return $this->getDataList();
    }
        
    public function FieldList() {
        return $this->fieldList;
    }
    
    public function performReadonlyTransformation() {
        $clone = clone $this;
        $clone->setShowPagination(false);

        $clone->addExtraClass( 'readonly' );
        $clone->setReadonly(true);
        return $clone;
    }
    
    /**
     * #################################
     *           Summary-Row
     * #################################
     */
    
    /**
     * Can utilize some built-in summary-functions, with optional casting. 
     * Currently supported:
     * - sum
     * - avg
     * 
     * @param $summaryTitle string
     * @param $summaryFields array 
     * Simple Format: array("MyFieldName"=>"sum")
     * With Casting: array("MyFieldname"=>array("sum","Currency->Nice"))
     */
    public function addSummary($summaryTitle, $summaryFieldList) {
        $this->summaryTitle = $summaryTitle;
        $this->summaryFieldList = $summaryFieldList;
    }
    
    public function removeSummary() {
        $this->summaryTitle = null;
        $this->summaryFields = null;
    }
    
    public function HasSummary() {
        return (isset($this->summaryFieldList));
    }
    
    public function SummaryTitle() {
        return $this->summaryTitle;
    }
    
    /**
     * @param SS_List $items Only used to pass grouped sourceItems for creating
     * partial summaries.
     */
    public function SummaryFields($items = null) {
        if(!isset($this->summaryFieldList)) {
            return false;
        }
        $summaryFields = array();
        $fieldListWithoutFirst = $this->fieldList;
        if(!empty($this->summaryTitle)) {
            array_shift($fieldListWithoutFirst);
        }
        foreach($fieldListWithoutFirst as $fieldName => $fieldTitle) {
            $fieldClasses = "";
            if(is_array($fieldTitle)){
                $fieldClasses = is_array($fieldTitle['classes']) ? implode(" ", $fieldTitle['classes']) : $fieldTitle['classes'];
                $fieldTitle = $fieldTitle['title'];
            }
            
            if(in_array($fieldName, array_keys($this->summaryFieldList))) {
                if(is_array($this->summaryFieldList[$fieldName])) {
                    $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName][0]}";
                    $casting = $this->summaryFieldList[$fieldName][1];
                } else {
                    $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName]}";
                    $casting = null;
                }

                // fall back to integrated sourceitems if not passed
                if(!$items) $items = $this->sourceItems();

                $summaryValue = ($items) ? $this->$summaryFunction($items->column($fieldName)) : null;
                
                // Optional casting, Format: array('MyFieldName'=>array('sum','Currency->Nice'))
                if(isset($casting)) {
                    $summaryValue = $this->getCastedValue($summaryValue, $casting); 
                }
            } else {
                $summaryValue = null;
                $function = null;
            }
            
            $summaryFields[] = new ArrayData(array(
                'Function' => $function,
                'SummaryValue' => $summaryValue,
                'Name' => DBField::create_field('Varchar', $fieldName),
                'Title' => DBField::create_field('Varchar', $fieldTitle),
                'Classes' => $fieldClasses
            ));
        }
        return new ArrayList($summaryFields);
    }
    
    public function HasGroupedItems() {
        return ($this->groupByField);   
    }
    
    public function GroupedItems() {
        if(!$this->groupByField) {
            return false; 
        }
        
        $items = $this->sourceItems();
        if(!$items || !$items->Count()) {
            return false;
        }
        
        $groupedItems = $items->groupBy($this->groupByField);
        $groupedArrItems = new ArrayList();
        foreach($groupedItems as $key => $group) {
            $fieldItems = new ArrayList();
            foreach($group as $item) {
                if($item) $fieldItems->push(new $this->itemClass($item, $this));
            }
            $groupedArrItems->push(new ArrayData(array(
                'Items' => $fieldItems,
                'SummaryFields' => $this->SummaryFields($group)
            )));
        }
        
        return $groupedArrItems;
    }
    
    public function colFunction_sum($values) {
        return array_sum($values);
    }

    public function colFunction_avg($values) {
        return array_sum($values)/count($values);
    }

    /**
     * #################################
     *           Pagination
     * #################################
     */
    public function setShowPagination($bool) {
        $this->showPagination = (bool)$bool;
        return $this;
    }

    /**
     * @return boolean
     */
    public function ShowPagination() {
        if($this->showPagination && !empty($this->summaryFieldList)) {
            user_error("You can't combine pagination and summaries - please disable one of them.", E_USER_ERROR);
        }
        return $this->showPagination;
    }
    
    public function setPageSize($pageSize) {
        $this->pageSize = $pageSize;
        return $this;
    }
    
    public function PageSize() {
        return $this->pageSize;
    }
    
    public function ListStart() {
        return $_REQUEST['ctf'][$this->getName()]['start'];
    }
    
    /**
     * @return array
     */
    public function getExtraLinkParams(){
        return $this->extraLinkParams;
    }
    
    public function FirstLink() {
        $start = 0;
        
        if(!isset($_REQUEST['ctf'][$this->getName()]['start'])
                || !is_numeric($_REQUEST['ctf'][$this->getName()]['start']) 
                || $_REQUEST['ctf'][$this->getName()]['start'] == 0) {

            return null;
        }
        $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
        $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
        if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
        
        // preserve sort options
        if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
            $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
            // direction
            if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
                $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
            }
        }
        
        return $link;
    }
    
    public function PrevLink() {
        if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
            $currentStart = $_REQUEST['ctf'][$this->getName()]['start'];
        } else {
            $currentStart = 0;
        }

        if($currentStart == 0) {
            return null;
        }
        
        if($_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize < 0) {
            $start = 0;
        } else {
            $start = $_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize;
        }
        
        $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
        $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
        if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
        
        // preserve sort options
        if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
            $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
            // direction
            if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
                $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
            }
        }
        
        return $link;
    }
    

    public function NextLink() {
        $currentStart = isset($_REQUEST['ctf'][$this->getName()]['start'])
            ? $_REQUEST['ctf'][$this->getName()]['start'] 
            : 0;
        $start = ($currentStart + $this->pageSize < $this->TotalCount())
            ? $currentStart + $this->pageSize
            : 0;
        if(!$start) {
            return null;
        }
        $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
        $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
        if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
        
        // preserve sort options
        if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
            $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
            // direction
            if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
                $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
            }
        }
        
        return $link;
    }
    
    public function LastLink() {
        $pageSize = ($this->TotalCount() % $this->pageSize > 0)
            ? $this->TotalCount() % $this->pageSize
            : $this->pageSize;
        $start = $this->TotalCount() - $pageSize;
        // Check if there is only one page, or if we are on last page
        if($this->TotalCount() <= $pageSize || (isset($_REQUEST['ctf'][$this->getName()]['start'])
                && $_REQUEST['ctf'][$this->getName()]['start'] >= $start)) {

            return null;
        }
        
        $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
        $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
        if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
        
        // preserve sort options
        if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
            $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
            // direction
            if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
                $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
            }
        }
        
        return $link;
    }
    
    public function FirstItem() {
        if ($this->TotalCount() < 1) return 0;
        return isset($_REQUEST['ctf'][$this->getName()]['start'])
            ? $_REQUEST['ctf'][$this->getName()]['start'] + 1
            : 1;
    }
    
    public function LastItem() {
        if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
            $pageStep = min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->getName()]['start']);
            return $_REQUEST['ctf'][$this->getName()]['start'] + $pageStep;
        } else {
            return min($this->pageSize, $this->TotalCount());
        }
    }

    /**
     * @ignore
     */
    private $_cache_TotalCount;

    /**
     * Return the total number of items in the source DataList
     */
    public function TotalCount() {
        if($this->_cache_TotalCount === null) {
            $this->_cache_TotalCount = $this->getDataList()->Count();
        }
        return $this->_cache_TotalCount;
    }
    
    /**
     * #################################
     *           CSV Export
     * #################################
     */
    public function setFieldListCsv($fields) {
        $this->fieldListCsv = $fields;
        return $this;
    }
    
    /**
     * Set the CSV separator character.  Defaults to ,
     */
    public function setCsvSeparator($csvSeparator) {
        $this->csvSeparator = $csvSeparator;
        return $this;
    }
    
    /**
     * Get the CSV separator character.  Defaults to ,
     */
    public function getCsvSeparator() {
        return $this->csvSeparator;
    }
    
    /**
     * Remove the header row from the CSV export
     */
    public function removeCsvHeader() {
        $this->csvHasHeader = false;
        return $this;
    }
    
    /**
     * Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField).
     * Uses {$csv_columns} if present, and falls back to {$result_columns}.
     * We move the most filedata generation code to the function {@link generateExportFileData()} so that a child class
     * could reuse the filedata generation code while overwrite export function.
     * 
     * @todo Make relation-syntax available (at the moment you'll have to use custom sql) 
     */
    public function export() {
        $now = date("d-m-Y-H-i");
        $fileName = "export-$now.csv";

        // No pagination for export
        $oldShowPagination = $this->showPagination;
        $this->showPagination = false;
        
        $result = $this->renderWith(array($this->template . '_printable', 'DataListField_printable'));
        
        $this->showPagination = $oldShowPagination;
        
        if($fileData = $this->generateExportFileData($numColumns, $numRows)){
            return SS_HTTPRequest::send_file($fileData, $fileName, 'text/csv');
        } else {
            user_error("No records found", E_USER_ERROR);
        }
    }
    
    public function generateExportFileData(&$numColumns, &$numRows) {
        $separator = $this->csvSeparator;
		$csvColumns = array();
		if($this->fieldListCsv){
			$csvColumns = $this->fieldListCsv;
		} else {
			foreach($this->fieldList as $key => $val){
				if(is_array($val)){
					$csvColumns[$key] = isset($val['title']) ? $val['title'] : $key;
				} else {
					$csvColumns[$key] = $val;
				}
			}
		}

        $fileData = '';
        $columnData = array();
        $fieldItems = new ArrayList();
        if($this->csvHasHeader) {
            $fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\"";
            $fileData .= "\n";
        }

        if(isset($this->customSourceItems)) {
            $items = $this->customSourceItems;
        } else {
            $items = $this->getCsvDataList();
        }
        
        // temporary override to adjust DataListField_Item behaviour
        $this->setFieldFormatting(array());
        $this->fieldList = $csvColumns;

        if($items) {
            foreach($items as $item) {
                if(is_array($item)) {
                    $className = isset($item['RecordClassName']) ? $item['RecordClassName'] : $item['ClassName'];
                    $item = new $className($item);
                }
                $fieldItem = new $this->itemClass($item, $this);
                
                $fields = $fieldItem->Fields(false);
                $columnData = array();
                if($fields) foreach($fields as $field) {
                    $value = $field->Value;
                    
                    // TODO This should be replaced with casting
                    if(array_key_exists($field->Name, $this->csvFieldFormatting)) {
                        $format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$field->Name]);
                        $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
                        $format = str_replace('__VAL__', '$value', $format);
                        eval('$value = "' . $format . '";');
                    }
                    
                    $value = str_replace(array("\r", "\n"), "\n", $value);
                    $tmpColumnData = '"' . str_replace('"', '\"', $value) . '"';
                    $columnData[] = $tmpColumnData;
                }
                $fileData .= implode($separator, $columnData);
                $fileData .= "\n";
                
                $item->destroy();
                unset($item);
                unset($fieldItem);
            }
            
            $numColumns = count($columnData);
            $numRows = $fieldItems->count();
            return $fileData;
        } else {
            return null;
        }
    }
    
    /**
     * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
     */
    public function ExportLink() {
        $exportLink = Controller::join_links($this->Link(), 'export');
        
        if($this->extraLinkParams) $exportLink .= "?" . http_build_query($this->extraLinkParams);
        return $exportLink;
    }
    
    /**
     * #################################
     *           Utilty
     * #################################
     */
    public function Utility() {
        $links = new ArrayList();
		if($this->getAllowedExport()){
	        $links->push(new ArrayData( array(
	            'Title' => _t('DataListField.EXPORT', 'Export to CSV'),
	            'Link' => $this->ExportLink()
	        )));
		}
        return $links;

    }
    
    public function setFieldCasting($casting) {
        $this->fieldCasting = $casting;
        return $this;
    }

    public function setFieldFormatting($formatting) {
        $this->fieldFormatting = $formatting;
        return $this;
    }
    
    public function setCSVFieldFormatting($formatting) {
        $this->csvFieldFormatting = $formatting;
        return $this;
    }
    
    /**
     * Edit the field list
     */
    public function setFieldList($fieldList) {
        $this->fieldList = $fieldList;
        return $this;
    }
    
    /**
     * @return String
     */
    public function Name() {
        return $this->name;
    }
    
    public function Title() {
        // adding translating functionality
        // this is a bit complicated, because this parameter is passed to this class
        // and should come here translated already
        // adding this to TODO probably add a method to the classes
        // to return they're translated string
        // added by ruibarreiros @ 27/11/2007
        return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName();
    }
    
    public function NameSingular() {
        // same as Title()
        // added by ruibarreiros @ 27/11/2007
        return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName();
    }

    public function NamePlural() {
        // same as Title()
        // added by ruibarreiros @ 27/11/2007
        return $this->sourceClass() ? singleton($this->sourceClass())->plural_name() : $this->getName();
    } 
    
    public function setTemplate($template) {
        $this->template = $template;
        return $this;
    }
    
    public function CurrentLink() {
        $link = $this->Link();
        
        if(isset($_REQUEST['ctf'][$this->getName()]['start'])
                && is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) {

            $start = ($_REQUEST['ctf'][$this->getName()]['start'] < 0)
                ? 0
                : $_REQUEST['ctf'][$this->getName()]['start'];
            $link = Controller::join_links($link, "?ctf[{$this->getName()}][start]={$start}");
        }

        if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
        
        return $link;
    }

    /**
     * Overloaded to automatically add security token.
     * 
     * @param String $action
     * @return String
     */
    public function Link($action = null) {
        $form = $this->getForm();
        if($form) {
            $token = $form->getSecurityToken();
            $parentUrlParts = parse_url(parent::Link($action));
            $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
            // Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks.
            if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart);
            return Controller::join_links($parentUrlParts['path'], $queryPart);
        } else {
            // allow for instanciation of this FormField outside of a controller/form
            // context (e.g. for unit tests)
            return false;
        }
    }

    /**
     * @param $value
     * 
     */
    public function getCastedValue($value, $castingDefinition) {
        if(is_array($castingDefinition)) {
            $castingParams = $castingDefinition;
            array_shift($castingParams);
            $castingDefinition = array_shift($castingDefinition);
        } else {
            $castingParams = array();
        }
        if(strpos($castingDefinition,'->') === false) {
            $castingFieldType = $castingDefinition;
            $castingField = DBField::create_field($castingFieldType, $value);
            $value = call_user_func_array(array($castingField,'XML'),$castingParams);
        } else {
            $fieldTypeParts = explode('->', $castingDefinition);
            $castingFieldType = $fieldTypeParts[0]; 
            $castingMethod = $fieldTypeParts[1];
            $castingField = DBField::create_field($castingFieldType, $value);
            $value = call_user_func_array(array($castingField,$castingMethod),$castingParams);
        }
        
        return $value;
    }    
    
    public function setHighlightConditions($conditions) {
        $this->highlightConditions = $conditions;
        return $this;
    }
    
    /**
     * See {@link SelectOptions()} for introduction.
     * 
     * @param $options array Options to add, key being a unique identifier of the action,
     *  and value a title for the rendered link element (can contain HTML).
     *  The keys for 'all' and 'none' have special behaviour associated
     *  through DataListField.js JavaScript.
     *  For any other key, the JavaScript automatically checks all checkboxes contained in
     *  <td> elements with a matching classname.
     */
    public function addSelectOptions($options){
        foreach($options as $k => $title)
        $this->selectOptions[$k] = $title;
		return $this;
    }
    
    /**
     * Remove one all more table's {@link $selectOptions}
     * 
     * @param $optionsNames array
     */
    public function removeSelectOptions($names){
        foreach($names as $name){
            unset($this->selectOptions[trim($name)]);
        }
    }
    
    /**
     * Return the table's {@link $selectOptions}.
     * Used to toggle checkboxes for each table row through button elements.
     * 
     * Requires {@link Markable()} to return TRUE.
     * This is only functional with JavaScript enabled.
     * 
     * @return SS_List of ArrayData objects
     */
    public function SelectOptions(){
        if(!$this->selectOptions) return;
        
        $selectOptionsSet = new ArrayList();
        foreach($this->selectOptions as $k => $v) {
            $selectOptionsSet->push(new ArrayData(array(
                'Key' => $k,
                'Value' => $v
            )));
        }
        return $selectOptionsSet;
    }
	
	public function setAllowedExport($allowed){
		$this->allowedExport = $allowed;
		return true;
	}
	
	public function getAllowedExport(){
		return $this->allowedExport;
	}
}

/**
 * A single record in a DataListField.
 * @package forms
 * @subpackage fields-relational
 * @see DataListField
 */
class DataListField_Item extends ViewableData {
    
    /**
     * @var DataObject The underlying data record,
     * usually an element of {@link DataListField->sourceItems()}.
     */
    protected $item;
    
    /**
     * @var DataListField
     */
    protected $parent;
    
    public function __construct($item, $parent) {
        $this->failover = $this->item = $item;
        $this->parent = $parent;
        parent::__construct();
    }
    
    public function ID() {
        return $this->item->ID;
    }
    
    public function Parent() {
        return $this->parent;
    }
    
    public function Fields($xmlSafe = true) {
        $list = $this->parent->FieldList();
        foreach($list as $fieldName => $fieldTitle) {
            $value = "";
            $type = "";
            
            $fieldClasses = "";
            if(is_array($fieldTitle)){
                $fieldClasses = is_array($fieldTitle['classes']) ? implode(" ", $fieldTitle['classes']) : $fieldTitle['classes'];
                $fieldTitle = $fieldTitle['title'];
            }
            
            // This supports simple FieldName syntax
            if(strpos($fieldName,'.') === false) {
                $type = $this->item->obj($fieldName)->class;
                $value = ($this->item->XML_val($fieldName) && $xmlSafe)
                    ? $this->item->XML_val($fieldName)
                    : $this->item->RAW_val($fieldName);
            // This support the syntax fieldName = Relation.RelatedField
            } else {                    
                $fieldNameParts = explode('.', $fieldName)  ;
                $tmpItem = $this->item;
                for($j=0;$j<sizeof($fieldNameParts);$j++) {
                    $relationMethod = $fieldNameParts[$j];
                    $idField = $relationMethod . 'ID';
                    if($j == sizeof($fieldNameParts)-1) {
                        if($tmpItem){
                            $type = $tmpItem->obj($relationMethod)->class;
                            $value = $tmpItem->$relationMethod;
                        }
                    } else {
                        if($tmpItem) $tmpItem = $tmpItem->$relationMethod();
                    }
                }
            }
            
            // casting
            if(array_key_exists($fieldName, $this->parent->fieldCasting)) {
                $type = $this->parent->fieldCasting[$fieldName];
                $value = $this->parent->getCastedValue($value, $this->parent->fieldCasting[$fieldName]);
            } elseif(is_object($value) && method_exists($value, 'Nice')) {
                $value = $value->Nice();
            }
            
            // formatting
            $item = $this->item;
            if(array_key_exists($fieldName, $this->parent->fieldFormatting)) {
                $format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$fieldName]);
                $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
                $format = str_replace('__VAL__', '$value', $format);
                eval('$value = "' . $format . '";');
            }
            
            //escape
            if($escape = $this->parent->fieldEscape){
                foreach($escape as $search => $replace){
                    $value = str_replace($search, $replace, $value);
                }
            }
            
            $fields[] = new ArrayData(array(
                "Name" => $fieldName, 
                "Title" => $fieldTitle,
                "Type" => $type,
                "Classes" => $fieldClasses,
                "Value" => $value,
                "CsvSeparator" => $this->parent->getCsvSeparator(),
            ));
        }
        return new ArrayList($fields);
    }
    
    public function Link($action = null) {
        $form = $this->parent->getForm();
        if($form) {
            $token = $form->getSecurityToken();
            $parentUrlParts = parse_url($this->parent->Link());
            $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
            // Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks.
            if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart);
            return Controller::join_links($parentUrlParts['path'], 'item', $this->item->ID, $action, $queryPart);
        } else {
            // allow for instanciation of this FormField outside of a controller/form
            // context (e.g. for unit tests)
            return false;
        }
    }
}

/**
 * @package forms
 * @subpackage fields-relational
 */
class DataListField_ItemRequest extends RequestHandler {
    protected $ctf;
    protected $itemID;
    protected $methodName;
    
    private static $url_handlers = array(
        '$Action!' => '$Action',
        '' => 'index',
    );
    
    public function Link() {
        return Controller::join_links($this->ctf->Link(), 'item/' . $this->itemID);
    }
    
    public function __construct($ctf, $itemID) {
        $this->ctf = $ctf;
        $this->itemID = $itemID;
        
        parent::__construct();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Return the data object being manipulated
     */
    public function dataObj() {
        // used to discover fields if requested and for population of field
        if(is_numeric($this->itemID)) {
            // we have to use the basedataclass, otherwise we might exclude other subclasses 
            return $this->ctf->getDataList()->byId($this->itemID);
        }
        
    }

    /**
     * @return DataListField
     */
    public function getParentController() {
        return $this->ctf;
    }
}
?>